unit uModuloNumber;

interface

uses uNumber;

type

  TModuloBaseType = Integer;

  TModuloNumber = class(TNumber)
  private
    class var ModuloBase: TModuloBaseType;

    var FValue: TModuloBaseType;

    procedure FitToRange;
    function  FindInverseValue(AValue: TModuloBaseType):TModuloBaseType;

    function  GetValue: TModuloBaseType;
    procedure SetValue(AValue: TModuloBaseType);

  public
    class procedure SetModuloBase(ABase: TModuloBaseType);

    constructor Create; overload;
    constructor Create(AValue: TModuloBaseType); overload;

    function  CreateNumberOfSameType: TNumber; override;

    function  Abs: TNumberBase; override;
    procedure Add(ANumber: TNumber); override;
    procedure BecomeAdditiveIdentity; override;
    procedure BecomeMultiplicativeIdentity; override;
    procedure CopyFrom(ANumber: TNumber); override;
    procedure Clear; override;
    procedure ChangeSign; override;
    procedure DivideBy(ANumber: TNumber); override;
    procedure FromString(const AString: String); override;
    procedure Invert; override;
    function  IsAdditiveIdentity:boolean; override;
    function  IsMultiplicativeIdentity:boolean; override;
    procedure MultiplyBy(ANumber: TNumber); override;
    procedure Random; override;
    procedure Subtract(ANumber: TNumber); override;
    function  ToString(const AFormat: string = DEFAULT_NUMBER_FORMAT): string; override;

    property  Value:TModuloBaseType read GetValue write SetValue;
  end;

implementation

uses SysUtils, uNumberExceptions;

{ TModuloNumber }

function TModuloNumber.Abs: TNumberBase;
begin
  Result := FValue;
end;

procedure TModuloNumber.Add(ANumber: TNumber);
begin
  if (not self.Valid) or (not ANumber.IsValid) then
  begin
    raise ECalculatingWithInvalidNumbers.Create('Calculating with invalid numbers');
  end;

  if not(ANumber is TModuloNumber) then
  begin
    raise EIncompatibleNumberTypes.Create(GetIncompatibleNumberTypesMsg(ANumber));
  end;

  FValue := FValue + TModuloNumber(ANumber).FValue;
  FitToRange;
end;

procedure TModuloNumber.BecomeAdditiveIdentity;
begin
  FValue := 0;
  Self.Valid := true;
end;

procedure TModuloNumber.BecomeMultiplicativeIdentity;
begin
  FValue := 1;
  Self.Valid := true;
end;

procedure TModuloNumber.ChangeSign;
begin
  FValue := -1 * FValue;
  FitToRange;
end;

procedure TModuloNumber.Clear;
begin
  FValue := 0;
  Valid := true;
end;

procedure TModuloNumber.CopyFrom(ANumber: TNumber);
begin
  if not ANumber.IsValid then
  begin
    raise ECalculatingWithInvalidNumbers.Create('Copying an invalid number');
  end;

  if not(ANumber is TModuloNumber) then
  begin
    raise EIncompatibleNumberTypes.Create(GetIncompatibleNumberTypesMsg(ANumber));
  end;

  FValue := TModuloNumber(ANumber).FValue;
  Valid := true;
end;

constructor TModuloNumber.Create;
begin
  FValue := 0;
  Valid := true;
end;

constructor TModuloNumber.Create(AValue: TModuloBaseType);
begin
  FValue := AValue;
  FitToRange;
  Valid := true;
end;

function TModuloNumber.CreateNumberOfSameType: TNumber;
begin
  Result := TModuloNumber.Create;
end;

procedure TModuloNumber.DivideBy(ANumber: TNumber);
begin
  if (not self.Valid) or (not ANumber.IsValid) then
  begin
    raise ECalculatingWithInvalidNumbers.Create('Calculating with invalid numbers');
  end;

  if not(ANumber is TModuloNumber) then
  begin
    raise EIncompatibleNumberTypes.Create(GetIncompatibleNumberTypesMsg(ANumber));
  end;

  if TModuloNumber(ANumber).FValue = 0 then
  begin
    raise EDivisionByZero.Create('Division by zero');
  end;

  FValue := FValue * FindInverseValue(TModuloNumber(ANumber).Value);
  FitToRange;
end;

function TModuloNumber.FindInverseValue(AValue: TModuloBaseType): TModuloBaseType;
var
  inverseValue: TModuloBaseType;
begin
  Result := 0;
  for inverseValue := 1 to ModuloBase - 1 do
  begin
    if (AValue * inverseValue) mod ModuloBase = 1 then
    begin
      Result := inverseValue;
      break;
    end;
  end;

  if Result = 0 then
  begin
    //TODO: raise exception?
  end;
end;

procedure TModuloNumber.FitToRange;
begin
  FValue := FValue mod ModuloBase;

  if FValue < 0 then
  begin
    FValue := ModuloBase + FValue;
  end;
end;

procedure TModuloNumber.FromString(const AString: String);
begin
  try
    FValue := StrToInt(AString);
    FitToRange;
    Self.Valid := true;
  except
    Self.Valid := false;
    raise EInvalidStringValue.CreateFmt('"%s" is not a valid TModuloNumber value',[AString]);
  end;
end;

function TModuloNumber.GetValue: TModuloBaseType;
begin
  Result := FValue;    
end;

procedure TModuloNumber.Invert;
begin
  if not self.Valid then
  begin
    raise ECalculatingWithInvalidNumbers.Create('Calculating with invalid numbers');
  end;

  FValue := FindInverseValue(FValue);
end;

function TModuloNumber.IsAdditiveIdentity: boolean;
begin
  Result := FValue = 0;
end;

function TModuloNumber.IsMultiplicativeIdentity: boolean;
begin
  Result := FValue = 1;
end;

procedure TModuloNumber.MultiplyBy(ANumber: TNumber);
begin
  if (not self.Valid) or (not ANumber.IsValid) then
  begin
    raise ECalculatingWithInvalidNumbers.Create('Calculating with invalid numbers');
  end;

  if not(ANumber is TModuloNumber) then
  begin
    raise EIncompatibleNumberTypes.Create(GetIncompatibleNumberTypesMsg(ANumber));
  end;

  FValue := FValue * TModuloNumber(ANumber).FValue;
  FitToRange;
end;

procedure TModuloNumber.Random;
begin
  FValue := System.Random(ModuloBase);
  Self.Valid := true;
end;

class procedure TModuloNumber.SetModuloBase(ABase: TModuloBaseType);
begin
  if ABase < 2 then
  begin
    raise Exception.CreateFmt('Invalid modulo number base: %d. Modulo base must be greater than 2',[ABase]);
  end;
  ModuloBase := ABase;
end;

procedure TModuloNumber.SetValue(AValue: TModuloBaseType);
begin
  FValue := AValue;
  FitToRange;
end;

procedure TModuloNumber.Subtract(ANumber: TNumber);
begin
  if (not self.Valid) or (not ANumber.IsValid) then
  begin
    raise ECalculatingWithInvalidNumbers.Create('Calculating with invalid numbers');
  end;

  if not(ANumber is TModuloNumber) then
  begin
    raise EIncompatibleNumberTypes.Create(GetIncompatibleNumberTypesMsg(ANumber));
  end;

  FValue := FValue - TModuloNumber(ANumber).FValue;
  FitToRange;
end;

function TModuloNumber.ToString(const AFormat: string): string;
begin
  if Self.Valid then
    Result := IntToStr(FValue)
  else
    Result := 'NaN';
end;

initialization

  TModuloNumber.SetModuloBase(5);

end.
